Skip to content

Rework long.ToMetric for correct interpretation of decimals parameter#1700

Open
logiclrd wants to merge 7 commits into
Humanizr:mainfrom
logiclrd:tometric-long-redux
Open

Rework long.ToMetric for correct interpretation of decimals parameter#1700
logiclrd wants to merge 7 commits into
Humanizr:mainfrom
logiclrd:tometric-long-redux

Conversation

@logiclrd

Copy link
Copy Markdown
Contributor

This PR:

  • Replaces the test suite for the long.ToMetric extension method with a much more comprehensive set using a TheoryData generator, and uses an interpretation of the decimals parameter in line with int.ToMetric and double.ToMetric in the process.

  • Makes all the tests pass by reworking the implementation of the ToMetric(long, ..) extension method and its underlying helper BuildRepresentation(long, ..) correspondingly.

This amends the implementation merged in #1596 to correct this misinterpretation. Thanks to @petervanleeuwen for noticing it 🙂

Here is a checklist you should tick through before submitting a pull request:

  • Implementation is clean
  • Code adheres to the existing coding standards; e.g. no curlies for one-line blocks, no redundant empty lines between methods or code blocks, spaces rather than tabs, etc.
    • In ToMetric(this long, ..), there is a one-line block with braces. The reason is consistency with the existing code in ToMetric(this int, ..) and ToMetric(this double, ..). If you prefer, I can add a commit that updates all of them to follow this style point.
  • No Code Analysis warnings
  • There is proper unit test coverage
  • (N/A) If the code is copied from StackOverflow (or a blog or OSS) full disclosure is included. That includes required license files and/or file headers explaining where the code came from with proper attribution
  • There are very few or no comments (because comments shouldn't be needed if you write clean code)
    • I put a comment next to a loop whose purpose isn't hard to discern but for which understanding how one iteration's effects tie into the next would add a handful of unnecessary seconds to the reader's day.
    • I used comments to denote number ranges in the test case generator. The separation reads better to my eye. Can remove them if they're deemed inappropriate.
  • (N/A) Xml documentation is added/updated for the addition/change
  • Your PR is (re)based on top of the latest commits from the main branch (more info below)
  • Link to the issue(s) you're fixing from your PR description. Use fixes #<the issue number>
  • (N/A) Readme is updated if you change an existing feature or add a new one
  • Run either build.cmd or build.ps1 and ensure there are no test failures
    • I don't see build.cmd or build.ps1 in the source tree, but the solution builds and all tests discovered by the Test Explorer pass.

Fixes: #1699

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dafb015aea

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +371 to +372
if ((divisor > 1) && (fractionalPart >= divisor / 2))
number += Math.Sign(number);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use banker’s rounding for midpoint values at 0 decimals

The new decimals == 0 path rounds whenever fractionalPart >= divisor / 2, which always rounds halves away from zero. That makes midpoint cases inconsistent with the Math.Round behavior used by the double/int overloads (and by the new test expectation generator): for example, 2500L.ToMetric(decimals: 0) will become 3k instead of the ToEven result 2k (similarly -2500L becomes -3k). Since this change is specifically about aligning long rounding semantics, these .5 cases are now inaccurate.

Useful? React with 👍 / 👎.

@logiclrd

Copy link
Copy Markdown
Contributor Author

@codex Review please.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc29f960ae

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/Humanizer/MetricNumeralExtensions.cs Outdated
@logiclrd

logiclrd commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

Oops. Made a last-minute change before the preceding commit, didn't re-test. That'll teach me.

@logiclrd

Copy link
Copy Markdown
Contributor Author

@codex Review please.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31e79ad7be

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +375 to +381
if (fractionalPart > roundingPoint)
number += Math.Sign(number);
else if (fractionalPart == roundingPoint)
{
// Use banker's rounding for consistency with Math.Round used elsewhere on floats.
number += Math.Sign(number % 2);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Promote rounded 1000 values to next metric prefix

After the decimals == 0 rounding step, number can become 1000 (or -1000) but the code immediately returns with the current unitText, producing outputs like 1000k for inputs such as 999500. This is inconsistent with the double/int path (which re-normalizes after rounding) and yields an incorrect metric unit at scale boundaries; a post-rounding normalization check is needed before formatting.

Useful? React with 👍 / 👎.

@logiclrd

Copy link
Copy Markdown
Contributor Author

@codex Review please.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

logiclrd added 5 commits May 26, 2026 10:42
… be much more comprehensive by using a TheoryData generator, reworking it to follow the correct interpretation of the decimals parameter.

Reworked ToMetric(long, ..) and BuildRepresentation(long, ..) in MetricNumeralExtensions.cs to treat the decimals parameter as a precision rather than a requested fixed number of decimals, making all the new tests pass.

# Conflicts:
#	src/Humanizer/MetricNumeralExtensions.cs
#	tests/Humanizer.Tests/MetricNumeralTests.cs
…ts.cs to exercise cases where the rounding direction matters, with values that match expected behaviour for int.ToMetric and double.ToMetric (banker's rounding).

Updated BuildMetricRepresentation in MetricNumeralExtensions.cs to use banker's rounding in the case where the fractional part rounds away entirely into the integer part of the number.
…ts.cs to exercise scale changes necessitated by rounding (999.9 => 1000).

Updated BuildMetricRepresentation in MetricNumeralExtensions.cs to detect & handle when rounding has bumped the number from +/-999 to +/-1000.
…ralExtensions.cs that discovers scale to not force decimals onto numbers that don't require scaling.
@logiclrd
logiclrd force-pushed the tometric-long-redux branch from ebcbdb9 to 4ffedba Compare May 26, 2026 15:52
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 09b17115-b1a2-4994-b23b-f0f8716e127f

📥 Commits

Reviewing files that changed from the base of the PR and between dd29e69 and b6242b4.

📒 Files selected for processing (1)
  • tests/Humanizer.Tests/CoverageGapTests.cs
✅ Files skipped from review due to trivial changes (1)
  • tests/Humanizer.Tests/CoverageGapTests.cs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Consistent handling of zero values in metric formatting regardless of requested precision
    • Locale-aware raw number formatting for unscaled values, preserving optional spacing before units
    • More accurate rounding with carry propagation, banker’s midpoint handling, and automatic scale adjustment when rounding reaches the next unit
    • Cleaner trimming of trailing fractional zeros
  • Tests

    • Tests now generate long-to-metric cases dynamically, covering rounding, scale-edge shifts and negatives; updated expectation for a space-suffixed whole-number case

Walkthrough

long.ToMetric now treats decimals as precision like int.ToMetric: zero and scale-0 values format as raw input, BuildMetricRepresentation was rewritten to compute fractional digits, perform carry-propagation rounding (banker’s tie handling), trim trailing zeros, and tests were converted to dynamically generated cases.

Changes

Decimals parameter and rounding behavior

Layer / File(s) Summary
Zero and unscaled value handling
src/Humanizer/MetricNumeralExtensions.cs
ToMetric now returns input.ToString() for zero irrespective of decimals. BuildRepresentation formats scale-0 values using the locale number format plus optional WithSpace, removing decimals-driven zero-padding.
Metric representation rounding and formatting
src/Humanizer/MetricNumeralExtensions.cs
BuildMetricRepresentation defaults missing decimals to the computed exponent, builds a padded fractional-digit array, rounds by incrementing with carry propagation (including banker’s midpoint logic), trims trailing fractional zeros by reducing decimals, handles decimals == 0 rounding with possible scale shift when result reaches 1000, and formats decimals > 0 using the locale decimal separator and selected fractional substring.
Dynamic test case generation
tests/Humanizer.Tests/MetricNumeralTests.cs, tests/Humanizer.Tests/CoverageGapTests.cs
Added GenerateLongToMetricTestCases() returning TheoryData<long, int?, string> that iterates numeric ranges and precisions to produce expected metric-format strings (including targeted rounding and scale-shift edge cases) and mirrored negative cases; test now uses [MemberData]. Adjusted one coverage expectation for a WithSpace case.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A metric fix for longs at last,
No padded zeros holding fast,
Digits carry, rounds decide,
Trailing zeros step aside,
Tests hop in — precision passed!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: reworking long.ToMetric to correctly interpret the decimals parameter for consistent behavior with other ToMetric variants.
Description check ✅ Passed The description is directly related to the changeset, explaining the comprehensive test suite replacement and implementation rework to fix the decimals parameter interpretation issue.
Linked Issues check ✅ Passed The PR implementation addresses the core requirement from issue #1699: long.ToMetric now interprets decimals as desired precision (not fixed decimal places), matching int.ToMetric and double.ToMetric behavior.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to fixing the decimals parameter interpretation in long.ToMetric and its helper methods, with corresponding test updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@logiclrd

Copy link
Copy Markdown
Contributor Author

Rebased.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/Humanizer/MetricNumeralExtensions.cs (1)

205-212: ⚡ Quick win

Update the decimals XML docs for the long overload.

This overload’s behavior changed, but the parameter docs still describe decimals as a fixed decimal count. Please document that it now matches the int/double overload semantics and trims unnecessary trailing zeros.

As per coding guidelines, "Add XML documentation for new or modified public APIs in C#."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer/MetricNumeralExtensions.cs` around lines 205 - 212, Update the
XML docs on the long overload ToMetric(this long input, MetricNumeralFormats?
formats = null, int? decimals = null) to describe the new semantics of the
decimals parameter: note that it now follows the same behavior as the int/double
overloads (acts as a maximum number of decimal places rather than a fixed count)
and that the resulting representation will trim unnecessary trailing zeros; edit
the <param name="decimals"> tag in MetricNumeralExtensions.ToMetric to clearly
state these points and mention default behavior when null.
tests/Humanizer.Tests/MetricNumeralTests.cs (1)

84-213: 🏗️ Heavy lift

Keep the generated expectations independent from the implementation.

Most rows here rebuild the same scale/round/trim behavior that long.ToMetric now uses. That weakens the test oracle: if the production bug is copied into this generator, the theory still passes. Prefer deriving these cases from an independent reference where possible, such as parity checks against int.ToMetric/double.ToMetric on overlapping ranges, and keep the carry/scale transitions as explicit rows.

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Humanizer/MetricNumeralExtensions.cs`:
- Around line 390-405: The current rounding in the BuildMetricRepresentation
path for ToMetric(long ...) uses a simple half-up check
(fractionalPartCharacters[decimals.Value] >= '5'); change it to banker's
(ToEven) rounding for decimals > 0 by detecting the exact-half case: when the
digit at fractionalPartCharacters[decimals.Value] == '5' and all subsequent
fractionalPartCharacters after that are '0', then only round up if the last
retained digit (fractionalPartCharacters[decimals.Value - 1]) is odd; otherwise
leave it (no round); for non-exact-half cases (digit > '5' or trailing
non-zero), continue to round up as currently implemented while preserving the
existing carry loop and break logic.

---

Nitpick comments:
In `@src/Humanizer/MetricNumeralExtensions.cs`:
- Around line 205-212: Update the XML docs on the long overload ToMetric(this
long input, MetricNumeralFormats? formats = null, int? decimals = null) to
describe the new semantics of the decimals parameter: note that it now follows
the same behavior as the int/double overloads (acts as a maximum number of
decimal places rather than a fixed count) and that the resulting representation
will trim unnecessary trailing zeros; edit the <param name="decimals"> tag in
MetricNumeralExtensions.ToMetric to clearly state these points and mention
default behavior when null.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fe233a92-5101-477b-b6ae-90bb50165428

📥 Commits

Reviewing files that changed from the base of the PR and between f9292aa and 4ffedba.

📒 Files selected for processing (2)
  • src/Humanizer/MetricNumeralExtensions.cs
  • tests/Humanizer.Tests/MetricNumeralTests.cs

Comment thread src/Humanizer/MetricNumeralExtensions.cs
logiclrd added 2 commits May 26, 2026 11:20
…cs to exercise exact midpoint rounding cases.

Implemented Banker's rounding for the handling of the fractional part in the long.BuildMetricRepresentation extension method in MetricNumeralExtensions.cs.
…s in CoverageGapTests.cs to track the correction in the interpretation of the decimals parameter to long.ToMetric.
@logiclrd

logiclrd commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@clairernovotny Is there any chance this PR might be considered? 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

long.ToMetric interprets decimals as a required number of fixed decimals instead of desired precision

1 participant